package src.montecarlo;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math.stat.descriptive.DescriptiveStatisticsImpl;

import com.sun.org.apache.xpath.internal.functions.Function;
import com.sun.xml.internal.bind.v2.model.core.MaybeElement;

import src.utils.ProbabilityDistributionFunction;

public class MetropolisMonteCarlo {
	
	private int sampleSize;
	private double delta;
	private int stepsOmitted;
	private int acceptedPoints;
	private double initialPoint;
	
	private Random gen;
	private ProbabilityDistributionFunction function;
	private List<Double> results;
	private DescriptiveStatistics stats;
	
	private boolean alreadySimulated;
	private String outputFile;
	
	/*
	 * termalizacioTime: fraction of total samples that will be omitted to thermalize.
	 */
	public MetropolisMonteCarlo(ProbabilityDistributionFunction function,int sampleSize,double delta,double thermalizationTime){

		this.sampleSize = sampleSize;
		this.delta = delta;
		this.function = function;
		this.stepsOmitted = (int)(thermalizationTime*sampleSize);
		
		gen = new Random();	
		acceptedPoints = 0;
		stats = new DescriptiveStatisticsImpl();
		
		alreadySimulated = false;
		initialPoint = 0; //where to start sampling
	}

	/*
	 * returns the ratio of rejected/reported.
	 * generates results on output file
	 */
	public double metropolisAlgorithm(String outputFile) throws IOException{
		
		this.outputFile = outputFile;
		
		//clear, just in case some previous results were stored
		
		
		int numberOfSamples = 0;
		int rejected = 0;
		
		acceptedPoints = 0;
		
		//initial trial step: ideally should be close to the probability maximum
		double x = initialPoint;
		double previousProba = 0;

		do{
			//drift from the previous position
			double deltaX = (2*gen.nextDouble()-1)*delta; 
			
			double newProba = function.evaluate(x+deltaX);
			
			double probabilitiesRatio = newProba/previousProba; 
			
			if(probabilitiesRatio>1){
				previousProba = newProba;
				numberOfSamples++;
				x+=deltaX;
				accept(x);
			}else{//probabilitiesRatio<1
				double threshold = gen.nextDouble();
				if(probabilitiesRatio>threshold){
					previousProba = newProba;
					numberOfSamples++;
					x+=deltaX;
					accept(x);
				}else{
					//reject data point
					rejected++;
					numberOfSamples++;
				}
			}
		}while(acceptedPoints<sampleSize);
		
		FileOutputStream fos = new FileOutputStream(new File(outputFile));
		
		//thermalize: get a subList
		stats.setWindowSize(sampleSize-stepsOmitted);
		
		for(int i=0;i<stats.getN();i++)
			fos.write((stats.getElement(i)+"\n").getBytes());
		fos.flush();
		fos.close();
	
		alreadySimulated = true;
		
		return rejected*1.0/numberOfSamples;
	}
	
	private void accept(double x) {
		acceptedPoints++;
		stats.addValue(x);
	}	
	
	public void autoCorrelation(String outputFile) {
	
		if(!alreadySimulated) {
			System.out.println("Need to run the simulation first.");
			return;
		}
		MMCUtils helper = new MMCUtils(stats.getValues());
		try {
			helper.autoCorrelation(outputFile);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/*
	 * Sample the already simulated results, every n points, to avoid auto-correlation
	 */
	public void reSample(int n,String outputFile) throws IOException{
		
		FileOutputStream fos = new FileOutputStream(new File(outputFile));
		
		int newSize = results.size()/n;
		for(int i=0;i<newSize;i++){
			fos.write((results.get(i*n)+"\n").getBytes());
		}
	}
	public double getMean(){
		if(!alreadySimulated) {
			System.out.println("Need to run the simulation first.");
			return 0;
		}
	return stats.getMean();
	}
	public double getSigma(){
		if(!alreadySimulated) {
			System.out.println("Need to run the simulation first.");
			return 0;
		}
	return stats.getStandardDeviation();
	}
	public void setInitialStep(double firstStep){
		this.initialPoint = firstStep;
	}
	public void setThermalizationTime(double thermalizationTime){
		this.stepsOmitted = (int)(thermalizationTime*sampleSize);
	}
}
